package br.com.skolar.server;

/*
 * Copyright 2008 Pavel Jbanov.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import javax.persistence.OptimisticLockException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;

import br.com.skolar.client.LockException;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

/**
 * GuiceRemoteServiceServlet is a wrapper servlet for GWT RPC utilizing Guice
 * for managing RemoteService implementation object's lifecycle.
 */
@Singleton
public class GuiceRemoteServiceServlet extends RemoteServiceServlet {
	@Inject
	private Injector injector;

	@Override
	public String processCall(String payload) throws SerializationException {
		RPCRequest rpcRequest = RPC.decodeRequest(payload, null, this);
		SerializationPolicy serializationPolicy = rpcRequest.getSerializationPolicy();
		Method serviceMethod = rpcRequest.getMethod();
		Object[] args = rpcRequest.getParameters();
		Object target = null;
		try {
			target = getService(payload);
			Object result = serviceMethod.invoke(target, args);
			return RPC.encodeResponseForSuccess(serviceMethod, result, serializationPolicy);
		} catch (IllegalAccessException e) {
			SecurityException securityException = new SecurityException(
					formatIllegalAccessErrorMessage(target, serviceMethod));
			securityException.initCause(e);
			throw securityException;
		} catch (IllegalArgumentException e) {
			SecurityException securityException = new SecurityException(
				formatIllegalArgumentErrorMessage(target, serviceMethod, args));
			securityException.initCause(e);
			throw securityException;
		} catch (InvocationTargetException e) {
			e.printStackTrace();
			if (e.getCause() instanceof OptimisticLockException)
				return RPC.encodeResponseForFailure(serviceMethod, new LockException(), serializationPolicy);
			// Try to encode the caught exception
			return RPC.encodeResponseForFailure(serviceMethod, e.getCause(), serializationPolicy);
		}
	}

	private RemoteService getService(String payload) throws InvocationTargetException {
		try {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			ServerSerializationStreamReader sr = new ServerSerializationStreamReader(classLoader, this);
			sr.prepareToRead(payload);
			Class c = Class.forName(sr.readString());
			return injector.getInstance(c);
		} catch (Exception e) {
			throw new InvocationTargetException(e);
		}
	}

	// Strict copy from RPC private static methods
	
	@Override
	protected void doUnexpectedFailure(Throwable e) {
		// replace default RemoteServiceServlet error handling
		ServletContext servletContext = getThreadLocalRequest().getSession()
				.getServletContext();
		servletContext.log("Exception while dispatching incoming RPC call", e);
		HttpServletResponse response = getThreadLocalResponse();
		// Send Exception message with 500 status.
		try {
			response.setContentType("text/plain");
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			try {
				response.getOutputStream().write(
						e.getMessage().getBytes("UTF-8"));
			} catch (IllegalStateException ex) {
				response.getWriter().write(
						"response.getWriter() was previously used");
			}
		} catch (IOException ex) {
			String msg = "respondWithUnexpectedFailure failed while sending the previous failure to the client";
			servletContext.log(msg, ex);
		}
	}

	private static String formatIllegalAccessErrorMessage(Object target,
			Method serviceMethod) {
		StringBuffer sb = new StringBuffer();
		sb.append("Blocked attempt to access inaccessible method '");
		sb.append(getSourceRepresentation(serviceMethod));
		sb.append("'");

		if (target != null) {
			sb.append(" on target '");
			sb.append(printTypeName(target.getClass()));
			sb.append("'");
		}

		sb.append("; this is either misconfiguration or a hack attempt");

		return sb.toString();
	}

	private static String formatIllegalArgumentErrorMessage(Object target,
			Method serviceMethod, Object[] args) {
		StringBuffer sb = new StringBuffer();
		sb.append("Blocked attempt to invoke method '");
		sb.append(getSourceRepresentation(serviceMethod));
		sb.append("'");

		if (target != null) {
			sb.append(" on target '");
			sb.append(printTypeName(target.getClass()));
			sb.append("'");
		}

		sb.append(" with invalid arguments");

		if (args != null && args.length > 0) {
			sb.append(Arrays.asList(args));
		}

		return sb.toString();
	}

	private static String getSourceRepresentation(Method method) {
		return method.toString().replace('$', '.');
	}

	private static String printTypeName(Class<?> type) {
		// Primitives
		//
		if (type.equals(Integer.TYPE)) {
			return "int";
		} else if (type.equals(Long.TYPE)) {
			return "long";
		} else if (type.equals(Short.TYPE)) {
			return "short";
		} else if (type.equals(Byte.TYPE)) {
			return "byte";
		} else if (type.equals(Character.TYPE)) {
			return "char";
		} else if (type.equals(Boolean.TYPE)) {
			return "boolean";
		} else if (type.equals(Float.TYPE)) {
			return "float";
		} else if (type.equals(Double.TYPE)) {
			return "double";
		}

		// Arrays
		//
		if (type.isArray()) {
			Class<?> componentType = type.getComponentType();
			return printTypeName(componentType) + "[]";
		}

		// Everything else
		//
		return type.getName().replace('$', '.');
	}
}